package org.thoughtcrime.securesms.recipients

import android.content.Context
import android.net.Uri
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.collections.immutable.toImmutableList
import org.signal.core.models.ServiceId
import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.ServiceId.PNI
import org.signal.core.util.BidiUtil
import org.signal.core.util.UuidUtil
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.signal.core.util.nullIfBlank
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.GroupRecordContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColors.Id.Auto
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting
import org.thoughtcrime.securesms.database.RecipientTable.MissingRecipientException
import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState
import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState
import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode
import org.thoughtcrime.securesms.database.RecipientTable.VibrateState
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails
import org.thoughtcrime.securesms.database.model.RecipientRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.phonenumbers.NumberUtil
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.util.SignalE164Util
import org.thoughtcrime.securesms.util.UsernameUtil.isValidUsernameForSearch
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.util.OptionalUtil
import java.util.LinkedList
import java.util.Objects
import java.util.Optional

/**
 * A recipient represents something you can send messages to, or receive messages from. They could be individuals, groups, or even distribution lists.
 * This class is a snapshot of common state that is used to present recipients through the UI.
 *
 * It's important to note that this is only a snapshot, and the actual state of a recipient can change over time.
 * If you ever need to present a recipient, you should consider observing a [LiveRecipient], which will let you get up-to-date snapshots as the data changes.
 */
class Recipient(
  val id: RecipientId = RecipientId.UNKNOWN,
  val isResolving: Boolean = true,
  private val aciValue: ACI? = null,
  private val pniValue: PNI? = null,
  private val usernameValue: String? = null,
  private val e164Value: String? = null,
  private val emailValue: String? = null,
  private val groupIdValue: GroupId? = null,
  private val distributionListIdValue: DistributionListId? = null,
  private val participantIdsValue: List<RecipientId> = emptyList(),
  private val groupAvatarId: Optional<Long> = Optional.empty(),
  val isActiveGroup: Boolean = false,
  val isSelf: Boolean = false,
  val isBlocked: Boolean = false,
  val muteUntil: Long = 0,
  val messageVibrate: VibrateState = VibrateState.DEFAULT,
  val callVibrate: VibrateState = VibrateState.DEFAULT,
  private val messageRingtoneUri: Uri? = null,
  private val callRingtoneUri: Uri? = null,
  val expiresInSeconds: Int = 0,
  val expireTimerVersion: Int = 1,
  private val registeredValue: RegisteredState = RegisteredState.NOT_REGISTERED,
  val profileKey: ByteArray? = null,
  val expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null,
  private val groupName: String? = null,
  private val systemContactPhoto: Uri? = null,
  private val customLabel: String? = null,
  val contactUri: Uri? = null,
  val profileName: ProfileName = ProfileName.EMPTY,
  val profileAvatar: String? = null,
  val profileAvatarFileDetails: ProfileAvatarFileDetails = ProfileAvatarFileDetails.NO_DETAILS,
  val isProfileSharing: Boolean = false,
  val hiddenState: HiddenState = HiddenState.NOT_HIDDEN,
  val lastProfileFetchTime: Long = 0,
  private val notificationChannelValue: String? = null,
  private val sealedSenderAccessModeValue: SealedSenderAccessMode = SealedSenderAccessMode.UNKNOWN,
  private val capabilities: RecipientRecord.Capabilities = RecipientRecord.Capabilities.UNKNOWN,
  val storageId: ByteArray? = null,
  val mentionSetting: MentionSetting = MentionSetting.ALWAYS_NOTIFY,
  private val wallpaperValue: ChatWallpaper? = null,
  private val chatColorsValue: ChatColors? = null,
  val avatarColor: AvatarColor = AvatarColor.UNKNOWN,
  val about: String? = null,
  val aboutEmoji: String? = null,
  private val systemProfileName: ProfileName = ProfileName.EMPTY,
  private val systemContactName: String? = null,
  private val extras: Optional<Extras> = Optional.empty(),
  val hasGroupsInCommon: Boolean = false,
  val badges: List<Badge> = emptyList(),
  val isReleaseNotes: Boolean = false,
  val needsPniSignature: Boolean = false,
  private val callLinkRoomId: CallLinkRoomId? = null,
  private val groupRecord: Optional<GroupRecord> = Optional.empty(),
  val phoneNumberSharing: PhoneNumberSharingState = PhoneNumberSharingState.UNKNOWN,
  val nickname: ProfileName = ProfileName.EMPTY,
  val note: String? = null
) {

  /** The recipient's [ServiceId], which could be either an [ACI] or [PNI]. */
  val serviceId: Optional<ServiceId> = OptionalUtil.or(Optional.ofNullable(aciValue), Optional.ofNullable(pniValue))

  /** The recipient's [ACI], if present. */
  val aci: Optional<ACI> = Optional.ofNullable(aciValue)

  /** The recipient's [PNI], if present. */
  val pni: Optional<PNI> = Optional.ofNullable(pniValue)

  /** The recipient's [DistributionListId], if present. */
  val distributionListId: Optional<DistributionListId> = Optional.ofNullable(distributionListIdValue)

  /** The recipient's username (concatenated nickname + discriminator), if present. */
  val username: Optional<String> = OptionalUtil.absentIfEmpty(usernameValue)

  /** Where or not this recipient is a system contact. */
  val isSystemContact: Boolean = contactUri != null

  /** The recipient's e164, if present. */
  val e164: Optional<String> = Optional.ofNullable(e164Value)

  /** Whether or not we should show this user's e164 in the interface. */
  val shouldShowE164: Boolean = e164Value.isNotNullOrBlank() && (isSystemContact || phoneNumberSharing == PhoneNumberSharingState.ENABLED)

  /** The recipient's email, if present. Emails are only for legacy SMS contacts that were reached via email. */
  val email: Optional<String> = Optional.ofNullable(emailValue)

  /** The recipients groupId, if present. */
  val groupId: Optional<GroupId> = Optional.ofNullable(groupIdValue)

  /** Whether or not the recipient has an address that could allow them to be reached via SMS. */
  val hasSmsAddress: Boolean = e164Value.isNotNullOrBlank() || emailValue.isNotNullOrBlank()

  /** Whether or not an [e164] is present. */
  val hasE164: Boolean = e164.isPresent

  /** Whether or not a [serviceId] is present. */
  val hasServiceId: Boolean = serviceId.isPresent

  /** Whether or not an [aci] is present. */
  val hasAci: Boolean = aci.isPresent

  /** Whether or not a [pni] is present. */
  val hasPni: Boolean = pni.isPresent

  /** True if the recipient has a [serviceId] and no other identifier, otherwise false. */
  val isServiceIdOnly: Boolean = hasServiceId && !hasSmsAddress

  /** Whether this recipient's story should be hidden from the main story list. */
  val shouldHideStory: Boolean = extras.map { it.hideStory() }.orElse(false)

  /** Whether or not you've seen all of this recipient's current stories. */
  val hasViewedStory: Boolean = extras.map { obj: Extras -> obj.hasViewedStory() }.orElse(false)

  /** Whether this recipient represents a link to group call.  */
  val isCallLink: Boolean = callLinkRoomId != null

  /** Whether the recipient has been hidden from the contact list. */
  val isHidden: Boolean = hiddenState != HiddenState.NOT_HIDDEN

  /** Whether or not this is an unknown recipient. */
  val isUnknown: Boolean = id.isUnknown

  /** Whether the recipient represents an individual person (as opposed to a group or list). */
  val isIndividual: Boolean
    get() = !isGroup && !isCallLink && !isDistributionList && !isReleaseNotes

  /** Whether the recipient represents a group. It could be a Signal group or MMS group. */
  val isGroup: Boolean
    get() = resolved.groupIdValue != null

  /** Whether the recipient represents an MMS group. */
  val isMmsGroup: Boolean
    get() {
      val groupId = resolved.groupIdValue
      return groupId != null && groupId.isMms
    }

  /** Whether the recipient represents a Signal group. */
  val isPushGroup: Boolean
    get() {
      val groupId = resolved.groupIdValue
      return groupId != null && groupId.isPush
    }

  /** Whether the recipient represents a V1 Signal group. These types of groups were deprecated in 2020. */
  val isPushV1Group: Boolean
    get() {
      val groupId = resolved.groupIdValue
      return groupId != null && groupId.isV1
    }

  /** Whether the recipient represents a V2 Signal group. */
  val isPushV2Group: Boolean
    get() {
      val groupId = resolved.groupIdValue
      return groupId != null && groupId.isV2
    }

  /** Whether the recipient represents a distribution list (a specific list of people to send a story to). */
  val isDistributionList: Boolean
    get() = resolved.distributionListIdValue != null

  /** Whether the recipient represents the "My Story" distribution list. */
  val isMyStory: Boolean
    get() = resolved.distributionListIdValue == DistributionListId.from(DistributionListId.MY_STORY_ID)

  /** A group is considered "unknown" if we don't have any data to render it. */
  val isUnknownGroup: Boolean
    get() = if ((groupAvatarId.isPresent && groupAvatarId.get() != -1L) || groupName.isNotNullOrBlank()) {
      false
    } else {
      participantIdsValue.isEmpty() || participantIdsValue.size == 1 && participantIdsValue.contains(self().id)
    }

  /** Whether the group is inactive. Groups become inactive when you leave them. */
  val isInactiveGroup: Boolean
    get() = isGroup && !isActiveGroup

  /** A photo to render for this recipient. */
  val contactPhoto: ContactPhoto?
    get() = if (isSelf) {
      null
    } else if (groupIdValue != null && groupAvatarId.isPresent) {
      GroupRecordContactPhoto(groupIdValue, groupAvatarId.get())
    } else if (systemContactPhoto != null && SignalStore.settings.isPreferSystemContactPhotos) {
      SystemContactPhoto(id, systemContactPhoto, 0)
    } else if (profileAvatar != null && profileAvatarFileDetails.hasFile()) {
      ProfileContactPhoto(this)
    } else if (systemContactPhoto != null) {
      SystemContactPhoto(id, systemContactPhoto, 0)
    } else {
      null
    }

  /**
   * Whether or not a recipient (either individual or group) has a corresponding avatar
   */
  val hasAvatar: Boolean
    get() {
      return (isIndividual && profileAvatar != null) || (isGroup && groupAvatarId.orElse(0L) != 0L)
    }

  /** The URI of the ringtone that should be used when receiving a message from this recipient, if set. */
  val messageRingtone: Uri? by lazy {
    if (messageRingtoneUri != null && messageRingtoneUri.scheme != null && messageRingtoneUri.scheme!!.startsWith("file")) {
      null
    } else {
      messageRingtoneUri
    }
  }

  /** The URI of the ringtone that should be used when receiving a call from this recipient, if set. */
  val callRingtone: Uri? by lazy {
    if (callRingtoneUri != null && callRingtoneUri.scheme != null && callRingtoneUri.scheme!!.startsWith("file")) {
      null
    } else {
      callRingtoneUri
    }
  }

  /** Whether or not the chat for the recipient is currently muted based on the current time. */
  val isMuted: Boolean
    get() = System.currentTimeMillis() <= muteUntil

  /** The ID's of the members if this recipient is a group or distribution list, otherwise empty. */
  val participantIds: List<RecipientId>
    get() = ArrayList(participantIdsValue)

  /** The [ACI]'s of the members if this recipient is a group, otherwise empty. */
  val participantAcis: List<ServiceId>
    get() {
      return groupRecord
        .map { it.requireV2GroupProperties().getMemberServiceIds().toImmutableList() }
        .orElse(emptyList<ServiceId>().toImmutableList())
    }

  /** The [RegisteredState] of this recipient. Signal groups/lists are always registered. */
  val registered: RegisteredState
    get() = if (isPushGroup || isDistributionList) {
      RegisteredState.REGISTERED
    } else if (isMmsGroup) {
      RegisteredState.NOT_REGISTERED
    } else {
      registeredValue
    }

  /** Shorthand to check if a user has been explicitly marked registered. */
  val isRegistered: Boolean
    get() = registered == RegisteredState.REGISTERED

  /** Shorthand to check if a user has _not_ been explicitly marked unregistered. */
  val isMaybeRegistered: Boolean
    get() = registered != RegisteredState.NOT_REGISTERED

  /** Shorthand to check if a user has been explicitly marked unregistered. */
  val isUnregistered: Boolean
    get() = registered == RegisteredState.NOT_REGISTERED

  /** Whether or not to show a special verified badge, indicating this is a special conversation (like release notes or note to self). */
  val showVerified: Boolean = isReleaseNotes || isSelf

  /** The notification channel, if both set and supported by the system. Otherwise null. */
  val notificationChannel: String? = if (!NotificationChannels.supported()) null else notificationChannelValue

  /** The state around whether we can send sealed sender to this user. */
  val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) {
    SealedSenderAccessMode.DISABLED
  } else {
    sealedSenderAccessModeValue
  }

  /** The wallpaper to render as the chat background, if present. */
  val wallpaper: ChatWallpaper?
    get() {
      return if (wallpaperValue != null) {
        wallpaperValue
      } else if (isReleaseNotes) {
        null
      } else {
        SignalStore.wallpaper.getWallpaper()
      }
    }

  /** Whether or not [wallpaper] was a value set specifically for this recipient. In other words, false means that we're showing a default wallpaper. */
  val hasOwnWallpaper: Boolean = wallpaperValue != null

  /** A cheap way to check if wallpaper is set without doing any unnecessary proto parsing. */
  val hasWallpaper: Boolean
    get() = wallpaperValue != null || SignalStore.wallpaper.hasWallpaperSet()

  /** The color of the chat bubbles to use in a chat with this recipient. */
  val chatColors: ChatColors
    get() {
      return if (chatColorsValue != null && chatColorsValue.id !is Auto) {
        chatColorsValue
      } else if (chatColorsValue != null) {
        autoChatColor
      } else {
        val global = SignalStore.chatColors.chatColors
        if (global != null && global.id !is Auto) {
          global
        } else {
          autoChatColor
        }
      }
    }

  /**
   * The badge to feature on a recipient's avatar, if any.
   * This value respects the local user's [SignalStore.inAppPayments.getDisplayBadgesOnProfile()] preference.
   */
  val featuredBadge: Badge? get() {
    return if (isSelf && !SignalStore.inAppPayments.getDisplayBadgesOnProfile()) {
      null
    } else {
      badges.firstOrNull()
    }
  }

  /** A string combining the about emoji + text for displaying various places. */
  val combinedAboutAndEmoji: String? by lazy { listOf(aboutEmoji, about).filter { it.isNotNullOrBlank() }.joinToString(separator = " ").nullIfBlank() }

  /** Whether or not we should blur the recipient's avatar when showing it in the chat list and other locations. */
  val shouldBlurAvatar: Boolean
    get() {
      val showOverride = extras.isPresent && extras.get().manuallyShownAvatar()
      return !showOverride && !isSelf && !isProfileSharing && !isSystemContact && !hasGroupsInCommon && isRegistered
    }

  /** Whether or not the recipient's avatar should be shown in the chat list by default. Even if false, user can still manually choose to show the avatar */
  val shouldShowAvatarByDefault: Boolean
    get() {
      return (isSelf || isProfileSharing || isSystemContact || hasGroupsInCommon) && isRegistered
    }

  /** The chat color to use when the "automatic" chat color setting is active, which derives a color from the wallpaper. */
  private val autoChatColor: ChatColors
    get() = wallpaper?.autoChatColors ?: ChatColorsPalette.Bubbles.default.withId(Auto)

  /** A fully resolved copy of this recipient, if needed. */
  private val resolved: Recipient
    get() = if (isResolving) live().resolve() else this

  /** Convenience method to get a non-null [serviceId] hen you know it is there. */
  fun requireServiceId(): ServiceId {
    return resolved.aciValue ?: resolved.pniValue ?: throw MissingServiceIdError(id)
  }

  /** Convenience method to get a non-null [aci] hen you know it is there. */
  fun requireAci(): ACI {
    return resolved.aciValue ?: throw MissingAciError(id)
  }

  /** Convenience method to get a non-null [pni] when you know it is there. */
  fun requirePni(): PNI {
    return resolved.pniValue ?: throw MissingPniError(id)
  }

  /** Convenience method to get a non-null [e164] when you know it is there. */
  fun requireE164(): String {
    return resolved.e164Value ?: throw MissingE164Error(id)
  }

  /** Convenience method to get a non-null [email] when you know it is there. */
  fun requireEmail(): String {
    return resolved.emailValue ?: throw MissingEmailError(id)
  }

  /** Convenience method to get a non-null sms address (either e164 or email) when you know it is there. */
  fun requireSmsAddress(): String {
    return resolved.e164Value ?: resolved.emailValue ?: throw MissingSmsAddressError(id)
  }

  /** Convenience method to get a non-null [groupId] when you know it is there. */
  fun requireGroupId(): GroupId {
    return resolved.groupIdValue ?: throw MissingGroupIdError(id)
  }

  /** Convenience method to get a non-null distributionListId when you know it is there. */
  fun requireDistributionListId(): DistributionListId {
    return resolved.distributionListIdValue ?: throw MissingDistributionIdError(id)
  }

  /** Convenience method to get a non-null callLinkRoomId when you know it is there. */
  fun requireCallLinkRoomId(): CallLinkRoomId {
    return resolved.callLinkRoomId ?: throw MissingCallLinkRoomIdError(id)
  }

  /** Convenience method to get a non-null call conversation ID when you know it is there. */
  fun requireCallConversationId(): ByteArray {
    return if (isPushGroup) {
      requireGroupId().decodedId
    } else if (isCallLink) {
      requireCallLinkRoomId().encodeForProto().toByteArray()
    } else if (isIndividual) {
      requireServiceId().toByteArray()
    } else {
      throw IllegalStateException("Recipient does not support conversation id")
    }
  }

  /** A single string to represent the recipient, in order of precedence: Group ID > ServiceId > Phone > Email */
  fun requireStringId(): String {
    return when {
      resolved.isGroup -> resolved.requireGroupId().toString()
      resolved.serviceId.isPresent -> resolved.requireServiceId().toString()
      else -> resolved.requireSmsAddress()
    }
  }

  /** The name to show for a group. It will be the group name if present, otherwise we default to a list of shortened member names. */
  fun getGroupName(context: Context): String? {
    return if (groupIdValue != null && Util.isEmpty(groupName)) {
      val selfId = AppDependencies.recipientCache.getSelfId()
      val others = participantIdsValue
        .filter { id: RecipientId -> id != selfId }
        .take(MAX_MEMBER_NAMES)
        .map { resolved(it) }

      val shortNameCounts: MutableMap<String, Int> = HashMap()
      for (participant in others) {
        val shortName = participant.getShortDisplayName(context)
        val count = Objects.requireNonNull(shortNameCounts.getOrDefault(shortName, 0))
        shortNameCounts[shortName] = count + 1
      }

      val names: MutableList<String> = LinkedList()
      for (participant in others) {
        val shortName = participant.getShortDisplayName(context)
        val count = Objects.requireNonNull(shortNameCounts.getOrDefault(shortName, 0))
        if (count <= 1) {
          names.add(shortName)
        } else {
          names.add(participant.getDisplayName(context))
        }
      }

      if (participantIdsValue.stream().anyMatch { id: RecipientId -> id == selfId }) {
        names.add(context.getString(R.string.Recipient_you))
      }

      Util.join(names, ", ")
    } else if (!isResolving && isMyStory) {
      context.getString(R.string.Recipient_my_story)
    } else if (!isResolving && Util.isEmpty(groupName) && isCallLink) {
      context.getString(R.string.Recipient_signal_call)
    } else {
      groupName
    }
  }

  /** False iff it [getDisplayName] would fall back to e164, email, or unknown. */
  fun hasAUserSetDisplayName(context: Context): Boolean {
    return getGroupName(context).isNotNullOrBlank() ||
      nickname.toString().isNotNullOrBlank() ||
      systemContactName.isNotNullOrBlank() ||
      profileName.toString().isNotNullOrBlank()
  }

  fun isMatch(query: String): Boolean {
    if (query.isEmpty()) {
      return true
    }

    val lowercaseQuery = query.lowercase()
    val sortName = listOf(
      nickname.toString(),
      nickname.givenName,
      systemProfileName.toString(),
      systemProfileName.givenName,
      profileName.toString(),
      profileName.givenName,
      username.orElse("")
    ).firstOrNull { it.isNotNullOrBlank() }?.lowercase()

    return sortName?.contains(lowercaseQuery) == true ||
      e164.map { it.contains(query) }.orElse(false) ||
      email.map { it.contains(query) }.orElse(false)
  }

  /** A full-length display name to render for this recipient. */
  fun getDisplayName(context: Context): String {
    var name = getNameFromLocalData(context)
    if (Util.isEmpty(name)) {
      name = usernameValue
    }
    if (Util.isEmpty(name)) {
      name = getUnknownDisplayName(context)
    }
    return BidiUtil.isolateBidi(name)
  }

  fun hasNonUsernameDisplayName(context: Context): Boolean {
    return getNameFromLocalData(context).isNotNullOrBlank()
  }

  /** A full-length display name for this user, ignoring the username. */
  private fun getNameFromLocalData(context: Context): String? {
    var name = getGroupName(context)

    if (name.isNullOrBlank()) {
      name = nickname.toString()
    }

    if (name.isBlank() && systemContactName != null) {
      name = systemContactName
    }

    if (name.isBlank()) {
      name = profileName.toString()
    }

    if (name.isBlank() && e164Value.isNotNullOrBlank()) {
      name = SignalE164Util.prettyPrint(e164Value)
    }

    if (name.isBlank() && emailValue != null) {
      name = emailValue
    }

    return name
  }

  /** A display name to use when rendering a mention of this user. */
  fun getMentionDisplayName(context: Context): String {
    var name: String? = if (isSelf) profileName.toString() else getGroupName(context)
    name = BidiUtil.isolateBidi(name)

    if (name.isBlank()) {
      name = if (isSelf) getGroupName(context) else nickname.toString()
      name = BidiUtil.isolateBidi(name)
    }

    if (name.isBlank()) {
      name = if (isSelf) getGroupName(context) else systemContactName
      name = BidiUtil.isolateBidi(name)
    }

    if (name.isBlank()) {
      name = if (isSelf) getGroupName(context) else profileName.toString()
      name = BidiUtil.isolateBidi(name)
    }

    if (name.isBlank() && e164Value.isNotNullOrBlank()) {
      name = SignalE164Util.prettyPrint(e164Value)
    }

    if (name.isBlank()) {
      name = BidiUtil.isolateBidi(emailValue)
    }

    if (name.isBlank()) {
      name = BidiUtil.isolateBidi(context.getString(R.string.Recipient_unknown))
    }

    return name
  }

  /** A shortened [getDisplayName], preferring given names. */
  fun getShortDisplayName(context: Context): String {
    val name = listOf(
      getGroupName(context),
      nickname.givenName,
      nickname.toString(),
      systemProfileName.givenName,
      systemProfileName.toString(),
      profileName.givenName,
      profileName.toString(),
      username.orElse(null),
      getDisplayName(context)
    ).firstOrNull { it.isNotNullOrBlank() }

    return BidiUtil.isolateBidi(name)
  }

  private fun getUnknownDisplayName(context: Context): String {
    return if (registered == RegisteredState.NOT_REGISTERED) {
      context.getString(R.string.Recipient_deleted_account)
    } else {
      context.getString(R.string.Recipient_unknown)
    }
  }

  fun getFallbackAvatar(): FallbackAvatar {
    return if (isSelf) {
      FallbackAvatar.Resource.Local(avatarColor)
    } else if (isResolving) {
      FallbackAvatar.Transparent
    } else if (isDistributionList) {
      FallbackAvatar.Resource.DistributionList(avatarColor)
    } else if (isCallLink) {
      FallbackAvatar.Resource.CallLink(avatarColor)
    } else if (groupIdValue != null) {
      FallbackAvatar.Resource.Group(avatarColor)
    } else if (isGroup) {
      FallbackAvatar.Resource.Group(avatarColor)
    } else if (groupName.isNotNullOrBlank()) {
      FallbackAvatar.forTextOrDefault(groupName, avatarColor, FallbackAvatar.Resource.Group(avatarColor))
    } else if (!nickname.isEmpty) {
      FallbackAvatar.forTextOrDefault(nickname.toString(), avatarColor)
    } else if (systemContactName.isNotNullOrBlank()) {
      FallbackAvatar.forTextOrDefault(systemContactName, avatarColor)
    } else if (!profileName.isEmpty) {
      FallbackAvatar.forTextOrDefault(profileName.toString(), avatarColor)
    } else {
      FallbackAvatar.Resource.Person(avatarColor)
    }
  }

  /**
   * If this recipient is missing crucial data, this will return a populated copy. Otherwise it
   * returns itself.
   */
  fun resolve(): Recipient {
    return resolved
  }

  /** Forces retrieving a fresh copy of the recipient, regardless of its state. */
  fun fresh(): Recipient {
    return live().refresh().resolve()
  }

  /** Returns a live, observable copy of this recipient. */
  fun live(): LiveRecipient {
    return AppDependencies.recipientCache.getLive(id)
  }

  enum class HiddenState(private val value: Int) {
    NOT_HIDDEN(0),
    HIDDEN(1),
    HIDDEN_MESSAGE_REQUEST(2);

    fun serialize(): Int {
      return value
    }

    companion object {
      fun deserialize(value: Int): HiddenState {
        return when (value) {
          0 -> NOT_HIDDEN
          1 -> HIDDEN
          2 -> HIDDEN_MESSAGE_REQUEST
          else -> throw IllegalArgumentException()
        }
      }
    }
  }

  enum class Capability(private val value: Int) {
    UNKNOWN(0),
    SUPPORTED(1),
    NOT_SUPPORTED(2);

    fun serialize(): Int {
      return value
    }

    val isSupported: Boolean
      get() = this == SUPPORTED

    companion object {
      fun deserialize(value: Int): Capability {
        return when (value) {
          0 -> UNKNOWN
          1 -> SUPPORTED
          2 -> NOT_SUPPORTED
          else -> throw IllegalArgumentException()
        }
      }

      fun fromBoolean(supported: Boolean): Capability {
        return if (supported) SUPPORTED else NOT_SUPPORTED
      }
    }
  }

  class Extras private constructor(private val recipientExtras: RecipientExtras) {
    fun manuallyShownAvatar(): Boolean {
      return recipientExtras.manuallyShownAvatar
    }

    fun hideStory(): Boolean {
      return recipientExtras.hideStory
    }

    fun hasViewedStory(): Boolean {
      return recipientExtras.lastStoryView > 0L
    }

    override fun equals(o: Any?): Boolean {
      if (this === o) return true
      if (o == null || javaClass != o.javaClass) return false
      val that = o as Extras
      return manuallyShownAvatar() == that.manuallyShownAvatar() && hideStory() == that.hideStory() && hasViewedStory() == that.hasViewedStory()
    }

    override fun hashCode(): Int {
      return Objects.hash(manuallyShownAvatar(), hideStory(), hasViewedStory())
    }

    companion object {
      fun from(recipientExtras: RecipientExtras?): Extras? {
        return if (recipientExtras != null) {
          Extras(recipientExtras)
        } else {
          null
        }
      }
    }
  }

  fun hasSameContent(other: Recipient): Boolean {
    return id == other.id &&
      isResolving == other.isResolving &&
      isSelf == other.isSelf &&
      isBlocked == other.isBlocked &&
      muteUntil == other.muteUntil &&
      expiresInSeconds == other.expiresInSeconds &&
      profileAvatarFileDetails == other.profileAvatarFileDetails &&
      isProfileSharing == other.isProfileSharing &&
      hiddenState == other.hiddenState &&
      aciValue == other.aciValue &&
      usernameValue == other.usernameValue &&
      e164Value == other.e164Value &&
      emailValue == other.emailValue &&
      groupIdValue == other.groupIdValue &&
      participantIdsValue == other.participantIdsValue &&
      groupAvatarId == other.groupAvatarId &&
      messageVibrate == other.messageVibrate &&
      callVibrate == other.callVibrate &&
      messageRingtoneUri == other.messageRingtoneUri &&
      callRingtoneUri == other.callRingtoneUri &&
      registeredValue == other.registeredValue &&
      profileKey.contentEquals(other.profileKey) &&
      expiringProfileKeyCredential == other.expiringProfileKeyCredential &&
      groupName == other.groupName &&
      systemContactPhoto == other.systemContactPhoto &&
      customLabel == other.customLabel &&
      contactUri == other.contactUri &&
      profileName == other.profileName &&
      systemProfileName == other.systemProfileName &&
      profileAvatar == other.profileAvatar &&
      notificationChannelValue == other.notificationChannelValue &&
      sealedSenderAccessModeValue == other.sealedSenderAccessModeValue &&
      storageId.contentEquals(other.storageId) &&
      mentionSetting == other.mentionSetting &&
      wallpaperValue == other.wallpaperValue &&
      chatColorsValue == other.chatColorsValue &&
      avatarColor == other.avatarColor &&
      about == other.about &&
      aboutEmoji == other.aboutEmoji &&
      extras == other.extras &&
      hasGroupsInCommon == other.hasGroupsInCommon &&
      badges == other.badges &&
      isActiveGroup == other.isActiveGroup &&
      callLinkRoomId == other.callLinkRoomId &&
      phoneNumberSharing == other.phoneNumberSharing &&
      nickname == other.nickname &&
      note == other.note
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false

    other as Recipient

    if (id != other.id) return false

    return true
  }

  override fun hashCode(): Int {
    return id.hashCode()
  }

  private class MissingServiceIdError(recipientId: RecipientId) : AssertionError("Missing ServiceId for " + recipientId.serialize())
  private class MissingAciError(recipientId: RecipientId) : AssertionError("Missing ACI for " + recipientId.serialize())
  private class MissingPniError(recipientId: RecipientId) : AssertionError("Missing PNI for " + recipientId.serialize())
  private class MissingE164Error(recipientId: RecipientId) : AssertionError("Missing E164 for " + recipientId.serialize())
  private class MissingEmailError(recipientId: RecipientId) : AssertionError("Missing email for " + recipientId.serialize())
  private class MissingSmsAddressError(recipientId: RecipientId) : AssertionError("Missing sms address for " + recipientId.serialize())
  private class MissingGroupIdError(recipientId: RecipientId) : AssertionError("Missing groupId for " + recipientId.serialize())
  private class MissingDistributionIdError(recipientId: RecipientId) : AssertionError("Missing distributionId for " + recipientId.serialize())
  private class MissingCallLinkRoomIdError(recipientId: RecipientId) : AssertionError("Missing call link for " + recipientId.serialize())

  companion object {
    private val TAG = Log.tag(Recipient::class.java)

    @JvmField
    val UNKNOWN = Recipient()

    private const val MAX_MEMBER_NAMES = 10

    /**
     * Returns a [LiveRecipient], which contains a [Recipient] that may or may not be
     * populated with data. However, you can observe the value that's returned to be notified when the
     * [Recipient] changes.
     */
    @JvmStatic
    @AnyThread
    fun live(id: RecipientId): LiveRecipient {
      return AppDependencies.recipientCache.getLive(id)
    }

    /**
     * Returns a live recipient wrapped in an Observable. All work is done on the IO threadpool.
     */
    @JvmStatic
    @AnyThread
    fun observable(id: RecipientId): Observable<Recipient> {
      return live(id).observable().subscribeOn(Schedulers.io())
    }

    /**
     * Returns a fully-populated [Recipient]. May hit the disk, and therefore should be
     * called on a background thread.
     */
    @JvmStatic
    @WorkerThread
    fun resolved(id: RecipientId): Recipient {
      return live(id).resolve()
    }

    @JvmStatic
    @WorkerThread
    fun resolvedList(ids: Collection<RecipientId>): List<Recipient> {
      return ids.map { resolved(it) }
    }

    @JvmStatic
    @WorkerThread
    fun distributionList(distributionListId: DistributionListId): Recipient {
      val id = SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionListId)
      return resolved(id)
    }

    /**
     * Returns a fully-populated [Recipient] and associates it with the provided username.
     */
    @JvmStatic
    @WorkerThread
    fun externalUsername(serviceId: ServiceId, username: String): Recipient {
      val recipient = externalPush(serviceId)
      SignalDatabase.recipients.setUsername(recipient.id, username)
      return recipient
    }

    /**
     * Returns a fully-populated [Recipient] based off of a [SignalServiceAddress], creating one in the database if necessary.
     */
    @JvmStatic
    @WorkerThread
    fun externalPush(signalServiceAddress: SignalServiceAddress): Recipient {
      return externalPush(signalServiceAddress.serviceId, signalServiceAddress.number.orElse(null))
    }

    /**
     * Returns a fully-populated [Recipient] based off of a ServiceId, creating one in the database if necessary.
     */
    @JvmStatic
    @WorkerThread
    fun externalPush(serviceId: ServiceId): Recipient {
      return externalPush(serviceId, null)
    }

    /**
     * Returns a fully-populated [Recipient] based off of a ServiceId and phone number, creating one
     * in the database if necessary. We want both piece of information so we're able to associate them
     * both together, depending on which are available.
     *
     * In particular, while we may eventually get the ACI of a user created via a phone number
     * (through a directory sync), the only way we can store the phone number is by retrieving it from
     * sent messages and whatnot. So we should store it when available.
     */
    @JvmStatic
    @WorkerThread
    private fun externalPush(serviceId: ServiceId, e164: String?): Recipient {
      if (ACI.UNKNOWN == serviceId || PNI.UNKNOWN == serviceId) {
        throw AssertionError()
      }

      val recipientId = RecipientId.from(SignalServiceAddress(serviceId, e164))
      val resolved = resolved(recipientId)

      if (resolved.id != recipientId) {
        Log.w(TAG, "Resolved $recipientId, but got back a recipient with ${resolved.id}")
      }

      if (!resolved.isRegistered) {
        Log.w(TAG, "External push was locally marked unregistered. Marking as registered.")
        SignalDatabase.recipients.markRegistered(recipientId, serviceId)
      }

      return resolved
    }

    /**
     * Create a recipient with a full (ACI, PNI, E164) tuple. It is assumed that the association between the PNI and serviceId is trusted.
     * That means it must be from either storage service (with the verified field set) or a PNI verification message.
     */
    @JvmStatic
    @WorkerThread
    fun trustedPush(aci: ACI, pni: PNI?, e164: String?): Recipient {
      if (ACI.UNKNOWN == aci || PNI.UNKNOWN == pni) {
        throw AssertionError("Unknown serviceId!")
      }

      val recipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(aci, pni, e164)
      val resolved = resolved(recipientId)

      if (resolved.id != recipientId) {
        Log.w(TAG, "Resolved $recipientId, but got back a recipient with ${resolved.id}")
      }

      if (!resolved.isRegistered) {
        Log.w(TAG, "External push was locally marked unregistered. Marking as registered.")
        SignalDatabase.recipients.markRegistered(recipientId, aci)
      }

      return resolved
    }

    /**
     * A safety wrapper around [.external] for when you know you're using an
     * identifier for a system contact, and therefore always want to prevent interpreting it as a
     * UUID. This will crash if given a UUID.
     *
     * (This may seem strange, but apparently some devices are returning valid UUIDs for contacts)
     */
    @JvmStatic
    @WorkerThread
    fun externalContact(identifier: String): Recipient? {
      val id: RecipientId = if (UuidUtil.isUuid(identifier)) {
        throw AssertionError("UUIDs are not valid system contact identifiers!")
      } else if (NumberUtil.isValidEmail(identifier)) {
        SignalDatabase.recipients.getOrInsertFromEmail(identifier)
      } else {
        val e164 = SignalE164Util.formatAsE164(identifier) ?: return null
        SignalDatabase.recipients.getOrInsertFromE164(e164)
      }

      return resolved(id)
    }

    /**
     * A version of [external] that should be used when you know the
     * identifier is a groupId.
     *
     * Important: This will throw an exception if the groupId you're using could have been migrated.
     * If you're dealing with inbound data, you should be using
     * [.externalPossiblyMigratedGroup], or checking the database before
     * calling this method.
     */
    @JvmStatic
    @WorkerThread
    fun externalGroupExact(groupId: GroupId): Recipient {
      return resolved(SignalDatabase.recipients.getOrInsertFromGroupId(groupId))
    }

    /**
     * Will give you one of:
     * - The recipient that matches the groupId specified exactly
     * - The recipient whose V1 ID would map to the provided V2 ID
     * - The recipient whose V2 ID would be derived from the provided V1 ID
     * - A newly-created recipient for the provided ID if none of the above match
     *
     * Important: You could get back a recipient with a different groupId than the one you provided.
     * You should be very cautious when using the groupId on the returned recipient.
     */
    @JvmStatic
    @WorkerThread
    fun externalPossiblyMigratedGroup(groupId: GroupId): Recipient {
      val id = RecipientId.from(groupId)
      return try {
        resolved(id)
      } catch (ex: MissingRecipientException) {
        Log.w(TAG, "Could not find recipient ($id) for group $groupId. Clearing RecipientId cache and trying again.", ex)
        RecipientId.clearCache()
        resolved(SignalDatabase.recipients.getOrInsertFromPossiblyMigratedGroupId(groupId))
      }
    }

    /**
     * Returns a fully-populated [Recipient] based off of a string identifier, creating one in
     * the database if necessary. The identifier may be a uuid, phone number, email,
     * or serialized groupId.
     *
     * If the identifier is a UUID of a Signal user, prefer using
     * [.externalPush] or its overload, as this will let us associate
     * the phone number with the recipient.
     *
     * Important: If the identifier cannot be considered a valid UUID, groupId, email, or phone number,
     * this will return null.
     */
    @JvmStatic
    @WorkerThread
    fun external(identifier: String): Recipient? {
      val serviceId = ServiceId.parseOrNull(identifier, logFailures = false)

      val id: RecipientId = if (serviceId != null) {
        SignalDatabase.recipients.getOrInsertFromServiceId(serviceId)
      } else if (GroupId.isEncodedGroup(identifier)) {
        SignalDatabase.recipients.getOrInsertFromGroupId(GroupId.parseOrThrow(identifier))
      } else if (NumberUtil.isValidEmail(identifier)) {
        SignalDatabase.recipients.getOrInsertFromEmail(identifier)
      } else if (isValidUsernameForSearch(identifier)) {
        throw IllegalArgumentException("Creating a recipient based on username alone is not supported!")
      } else {
        val e164: String = SignalE164Util.formatAsE164(identifier) ?: return null
        SignalDatabase.recipients.getOrInsertFromE164(e164)
      }

      return resolved(id)
    }

    @JvmStatic
    fun self(): Recipient {
      return AppDependencies.recipientCache.getSelf()
    }

    /** Whether we've set a recipient for 'self' yet. We do this during registration. */
    val isSelfSet: Boolean
      get() = AppDependencies.recipientCache.getSelfId() != null
  }
}
